/*
 * 
 *  This file is part of Sourceprojects.org Lycia Copyright 2010 noctarius
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * 
 */
package org.sourceprojects.lycia.internal.generator;

import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicLong;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.util.CheckMethodAdapter;
import org.objectweb.asm.util.TraceMethodVisitor;
import org.sourceprojects.lycia.DefinitionBuilder;
import org.sourceprojects.lycia.Delegate;
import org.sourceprojects.lycia.Generator;
import org.sourceprojects.lycia.LyciaParser;
import org.sourceprojects.lycia.ParserContext;
import org.sourceprojects.lycia.QualifiedName;
import org.sourceprojects.lycia.exceptions.LyciaConfigurationException;
import org.sourceprojects.lycia.exceptions.LyciaParsingException;
import org.sourceprojects.lycia.internal.configuration.TagConfigurationUtils.MethodDefinition;
import org.sourceprojects.lycia.internal.privileged.CreateClassLoader;
import org.sourceprojects.lycia.internal.privileged.SetAccessible;
import org.sourceprojects.lycia.internal.validation.ConstraintValidatorUtils;
import org.sourceprojects.lycia.internal.validation.PropertyDescriptor;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * 
 * @author noctarius
 * @since 1.9.1
 * 
 * @param <T>
 */
public class BytecodeGenerator<B> implements Generator<B>, Opcodes {
	private static final String PARSE_SIGNATURE_DELEGATE = Type
			.getMethodDescriptor(Type.VOID_TYPE, new Type[] {
					Type.getType(Element.class), Type.getType(String.class),
					Type.getType(QualifiedName.class),
					Type.getType(ParserContext.class),
					Type.getType(DefinitionBuilder.class) });

	private static final String TRANSFORM_SIGNATURE_DELEGATE = Type
			.getMethodDescriptor(Type.getType(Node.class), new Type[] {
					Type.getType(Element.class), Type.getType(String.class),
					Type.getType(QualifiedName.class),
					Type.getType(ParserContext.class),
					Type.getType(DefinitionBuilder.class) });

	private static final String EXCEPTION_CLASS = Type.getType(
			LyciaParsingException.class).getInternalName();

	private static final String SUPER_CLASS = Type.getType(
			BytecodeDelegate.class).getInternalName();

	private static final String METHOD_UTILS_CLASS = Type.getType(
			MethodParameterUtils.class).getInternalName();

	private static final String PARSE_METHOD_NAME = "invokeParse";
	private static final String TRANSFORM_METHOD_NAME = "invokeTransform";
	private static final int METHOD_ACCESS = ACC_PUBLIC + ACC_FINAL;

	private static final String METHOD_UTILS_PARAMETER_DECLARATION = Type
			.getMethodDescriptor(Type.getType(Object[].class), new Type[] {
					Type.getType(Method.class), Type.getType(Element.class),
					Type.getType(String.class),
					Type.getType(ParserContext.class),
					Type.getType(DefinitionBuilder.class),
					Type.getType(LyciaParser.class) });

	private static final AtomicLong LONG = new AtomicLong(0);

	private final GeneratorClassLoader classloader;

	public BytecodeGenerator() {
		classloader = CreateClassLoader.createClassLoader(getClass()
				.getClassLoader());
	}

	@Override
	public Delegate<B> generate(final Object parser,
			final MethodDefinition methodDefinition,
			final LyciaParser<B> lyciaParser) {

		try {
			final long id = LONG.getAndIncrement();
			final StringBuilder classname = new StringBuilder();
			classname.append("$LYCIA$Bytecode$Generated$").append(id);

			if (!methodDefinition.getMethod().isAccessible())
				SetAccessible.setAccessible(methodDefinition.getMethod());

			final PropertyDescriptor<B>[] propertyDescriptors = ConstraintValidatorUtils
					.buildPropertyDescriptors(methodDefinition.getMethod(),
							lyciaParser);

			final ClassWriter cw = new ClassWriter(0);
			cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, classname.toString(),
					toGenerifiedInternalRepresentation(classname.toString()),
					SUPER_CLASS, null);

			// Adding standard constructor
			createConstructor(cw);

			// Adding methods
			if (methodDefinition.getMethod().getReturnType() != Element.class) {
				createParseMethod(cw, methodDefinition);
				callAReturn(cw);
			} else {
				callReturn(cw);
				createTransformMethod(cw, methodDefinition);
			}

			// Closing class visit
			cw.visitEnd();

			final byte[] bytecode = cw.toByteArray();

			final Class<? extends BytecodeDelegate<B>> clazz = classloader
					.loadClass(bytecode);
			final BytecodeDelegate<B> instance = clazz.newInstance();
			instance.setParser(parser);
			instance.setLyciaParser(lyciaParser);
			instance.setPropertyDescriptor(propertyDescriptors);

			return instance;

		} catch (final IllegalAccessException e) {
			throw new LyciaConfigurationException(
					"BytecodeGenerator could not generate Delegate for parser '"
							+ parser + "'", e);
		} catch (final InstantiationException e) {
			throw new LyciaConfigurationException(
					"BytecodeGenerator could not generate Delegate for parser '"
							+ parser + "'", e);
		}
	}

	private MethodVisitor createConstructor(final ClassVisitor cw) {
		final MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V",
				null, null);
		mv.visitCode();
		mv.visitVarInsn(ALOAD, 0);
		mv.visitMethodInsn(INVOKESPECIAL, SUPER_CLASS, "<init>", "()V");
		mv.visitInsn(RETURN);
		mv.visitMaxs(1, 1);
		mv.visitEnd();
		return mv;
	}

	private MethodVisitor createParseMethod(final ClassVisitor cw,
			final MethodDefinition methodDefinition) {

		final MethodVisitor mv = cw.visitMethod(METHOD_ACCESS,
				PARSE_METHOD_NAME, PARSE_SIGNATURE_DELEGATE, null,
				new String[] { EXCEPTION_CLASS });
		final TraceMethodVisitor tmv = new TraceMethodVisitor(mv);
		final CheckMethodAdapter cma = new CheckMethodAdapter(tmv);

		cma.visitCode();
		callParseParameter(cma, methodDefinition.getMethod());
		cma.visitEnd();
		return mv;
	}

	private void callParseParameter(final MethodVisitor mv, final Method method) {
		final Class<?>[] types = method.getParameterTypes();
		final String classname = method.getDeclaringClass().getCanonicalName();
		final String methodname = method.getName();

		final Label l0 = new Label();
		final Label l1 = new Label();
		final Label l2 = new Label();

		mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Exception");
		mv.visitLabel(l0);

		mv.visitVarInsn(ALOAD, 0);
		mv.visitMethodInsn(INVOKEVIRTUAL, SUPER_CLASS, "getParser",
				"()Ljava/lang/Object;");
		mv.visitTypeInsn(CHECKCAST, toInternalRepresentation(classname));
		mv.visitVarInsn(ASTORE, 6);

		mv.visitVarInsn(ALOAD, 0);
		mv.visitMethodInsn(INVOKEVIRTUAL, SUPER_CLASS, "getLyciaParser",
				"()Ljava/lang/Object;");
		mv.visitTypeInsn(CHECKCAST, Type.getInternalName(LyciaParser.class));
		mv.visitVarInsn(ASTORE, 7);

		mv.visitVarInsn(ALOAD, 6);
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass",
				"()Ljava/lang/Class;");
		mv.visitLdcInsn(methodname);
		mv.visitIntInsn(BIPUSH, types.length);
		mv.visitTypeInsn(ANEWARRAY, "java/lang/Class");

		for (int i = 0; i < types.length; i++) {
			mv.visitInsn(DUP);

			if (i < 5) {
				switch (i) {
				case 0:
					mv.visitInsn(ICONST_0);
					break;
				case 1:
					mv.visitInsn(ICONST_1);
					break;
				case 2:
					mv.visitInsn(ICONST_2);
					break;
				case 3:
					mv.visitInsn(ICONST_3);
					break;
				case 4:
					mv.visitInsn(ICONST_4);
					break;
				case 5:
					mv.visitInsn(ICONST_5);
				}
			} else {
				mv.visitIntInsn(BIPUSH, i);
			}

			if (types[i].isPrimitive()) {
				makePrimitiveClassDescriptor(types[i], mv);
			} else {
				mv.visitLdcInsn(Type.getType(Type.getDescriptor(types[i])));
			}

			mv.visitInsn(AASTORE);
		}

		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getMethod",
				"(Ljava/lang/String;[Ljava/lang/Class;)"
						+ "Ljava/lang/reflect/Method;");
		mv.visitVarInsn(ASTORE, 8);

		mv.visitVarInsn(ALOAD, 8);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitVarInsn(ALOAD, 2);
		mv.visitVarInsn(ALOAD, 4);
		mv.visitVarInsn(ALOAD, 5);
		mv.visitVarInsn(ALOAD, 7);

		mv.visitMethodInsn(INVOKESTATIC, METHOD_UTILS_CLASS,
				"generateParameter", METHOD_UTILS_PARAMETER_DECLARATION);
		mv.visitVarInsn(ASTORE, 9);

		mv.visitVarInsn(ALOAD, 6);
		for (int i = 0; i < types.length; i++) {
			mv.visitVarInsn(ALOAD, 9);

			if (i < 5) {
				switch (i) {
				case 0:
					mv.visitInsn(ICONST_0);
					break;
				case 1:
					mv.visitInsn(ICONST_1);
					break;
				case 2:
					mv.visitInsn(ICONST_2);
					break;
				case 3:
					mv.visitInsn(ICONST_3);
					break;
				case 4:
					mv.visitInsn(ICONST_4);
					break;
				case 5:
					mv.visitInsn(ICONST_5);
				}
			} else {
				mv.visitIntInsn(BIPUSH, i);
			}

			mv.visitInsn(AALOAD);

			if (types[i].isArray()) {
				mv.visitTypeInsn(CHECKCAST, Type.getDescriptor(types[i]));

			} else if (types[i].isPrimitive()) {
				makePrimitiveAutoboxing(types[i], mv);

			} else {
				mv.visitTypeInsn(CHECKCAST, toInternalRepresentation(types[i]
						.getCanonicalName()));
			}
		}

		mv.visitMethodInsn(INVOKEVIRTUAL, toInternalRepresentation(classname),
				methodname, Type.getMethodDescriptor(method));
		mv.visitLabel(l1);

		final Label l3 = new Label();
		mv.visitJumpInsn(GOTO, l3);
		mv.visitLabel(l2);

		mv.visitFrame(Opcodes.F_SAME1, 0, null, 1,
				new Object[] { "java/lang/Exception" });
		mv.visitVarInsn(ASTORE, 6);

		mv.visitTypeInsn(NEW, EXCEPTION_CLASS);
		mv.visitInsn(DUP);
		mv.visitVarInsn(ALOAD, 6);
		mv.visitMethodInsn(INVOKESPECIAL, EXCEPTION_CLASS, "<init>",
				"(Ljava/lang/Throwable;)V");
		mv.visitInsn(ATHROW);
		mv.visitLabel(l3);

		mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
		mv.visitInsn(RETURN);

		mv.visitMaxs(12, 10);
	}

	private MethodVisitor createTransformMethod(final ClassVisitor cw,
			final MethodDefinition methodDefinition) {

		final MethodVisitor mv = cw.visitMethod(METHOD_ACCESS,
				TRANSFORM_METHOD_NAME, TRANSFORM_SIGNATURE_DELEGATE, null,
				new String[] { EXCEPTION_CLASS });

		mv.visitCode();
		callTransformParameter(mv, methodDefinition.getMethod());
		mv.visitEnd();
		return mv;
	}

	private void callTransformParameter(final MethodVisitor mv,
			final Method method) {

		final Class<?>[] types = method.getParameterTypes();
		final String classname = method.getDeclaringClass().getCanonicalName();
		final String methodname = method.getName();

		final Label l0 = new Label();
		final Label l1 = new Label();
		final Label l2 = new Label();

		mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Exception");
		mv.visitLabel(l0);

		mv.visitVarInsn(ALOAD, 0);
		mv.visitMethodInsn(INVOKEVIRTUAL, SUPER_CLASS, "getParser",
				"()Ljava/lang/Object;");
		mv.visitTypeInsn(CHECKCAST, toInternalRepresentation(classname));
		mv.visitVarInsn(ASTORE, 6);

		mv.visitVarInsn(ALOAD, 0);
		mv.visitMethodInsn(INVOKEVIRTUAL, SUPER_CLASS, "getLyciaParser",
				"()Ljava/lang/Object;");
		mv.visitTypeInsn(CHECKCAST, Type.getInternalName(LyciaParser.class));
		mv.visitVarInsn(ASTORE, 7);

		mv.visitVarInsn(ALOAD, 6);
		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass",
				"()Ljava/lang/Class;");
		mv.visitLdcInsn(methodname);
		mv.visitIntInsn(BIPUSH, types.length);
		mv.visitTypeInsn(ANEWARRAY, "java/lang/Class");

		for (int i = 0; i < types.length; i++) {
			mv.visitInsn(DUP);

			if (i < 5) {
				switch (i) {
				case 0:
					mv.visitInsn(ICONST_0);
					break;
				case 1:
					mv.visitInsn(ICONST_1);
					break;
				case 2:
					mv.visitInsn(ICONST_2);
					break;
				case 3:
					mv.visitInsn(ICONST_3);
					break;
				case 4:
					mv.visitInsn(ICONST_4);
					break;
				case 5:
					mv.visitInsn(ICONST_5);
				}
			} else {
				mv.visitIntInsn(BIPUSH, i);
			}

			if (types[i].isPrimitive()) {
				makePrimitiveClassDescriptor(types[i], mv);
			} else {
				mv.visitLdcInsn(Type.getType(Type.getDescriptor(types[i])));
			}

			mv.visitInsn(AASTORE);
		}

		mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getMethod",
				"(Ljava/lang/String;[Ljava/lang/Class;)"
						+ "Ljava/lang/reflect/Method;");
		mv.visitVarInsn(ASTORE, 8);

		mv.visitVarInsn(ALOAD, 8);
		mv.visitVarInsn(ALOAD, 1);
		mv.visitVarInsn(ALOAD, 2);
		mv.visitVarInsn(ALOAD, 4);
		mv.visitVarInsn(ALOAD, 5);
		mv.visitVarInsn(ALOAD, 7);

		mv.visitMethodInsn(INVOKESTATIC, METHOD_UTILS_CLASS,
				"generateParameter", METHOD_UTILS_PARAMETER_DECLARATION);
		mv.visitVarInsn(ASTORE, 9);

		mv.visitVarInsn(ALOAD, 6);
		for (int i = 0; i < types.length; i++) {
			mv.visitVarInsn(ALOAD, 9);

			if (i < 5) {
				switch (i) {
				case 0:
					mv.visitInsn(ICONST_0);
					break;
				case 1:
					mv.visitInsn(ICONST_1);
					break;
				case 2:
					mv.visitInsn(ICONST_2);
					break;
				case 3:
					mv.visitInsn(ICONST_3);
					break;
				case 4:
					mv.visitInsn(ICONST_4);
					break;
				case 5:
					mv.visitInsn(ICONST_5);
				}
			} else {
				mv.visitIntInsn(BIPUSH, i);
			}

			mv.visitInsn(AALOAD);

			if (types[i].isArray()) {
				mv.visitTypeInsn(CHECKCAST, Type.getDescriptor(types[i]));

			} else if (types[i].isPrimitive()) {
				makePrimitiveAutoboxing(types[i], mv);

			} else {
				mv.visitTypeInsn(CHECKCAST, toInternalRepresentation(types[i]
						.getCanonicalName()));
			}
		}

		mv.visitMethodInsn(INVOKEVIRTUAL, toInternalRepresentation(classname),
				methodname, Type.getMethodDescriptor(method));
		mv.visitLabel(l1);

		final Label l3 = new Label();
		mv.visitJumpInsn(GOTO, l3);
		mv.visitLabel(l2);

		mv.visitFrame(Opcodes.F_SAME1, 0, null, 1,
				new Object[] { "java/lang/Exception" });
		mv.visitVarInsn(ASTORE, 6);

		mv.visitTypeInsn(NEW, EXCEPTION_CLASS);
		mv.visitInsn(DUP);
		mv.visitVarInsn(ALOAD, 6);
		mv.visitMethodInsn(INVOKESPECIAL, EXCEPTION_CLASS, "<init>",
				"(Ljava/lang/Throwable;)V");
		mv.visitInsn(ATHROW);
		mv.visitLabel(l3);

		mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
		mv.visitInsn(ARETURN);

		mv.visitMaxs(12, 10);
	}

	private String toInternalRepresentation(final String classname) {
		return classname.replace(".", "/");
	}

	private String toGenerifiedInternalRepresentation(final String classname) {
		return "<T:Ljava/lang/Object;>L" + toInternalRepresentation(classname)
				+ "<TT;>;";
	}

	private void callReturn(final ClassVisitor cw) {
		final MethodVisitor mv = cw.visitMethod(METHOD_ACCESS,
				PARSE_METHOD_NAME, PARSE_SIGNATURE_DELEGATE, null,
				new String[] { EXCEPTION_CLASS });
		mv.visitInsn(RETURN);
		mv.visitMaxs(0, 6);
		mv.visitEnd();
	}

	private void callAReturn(final ClassVisitor cw) {
		final MethodVisitor mv = cw.visitMethod(METHOD_ACCESS,
				TRANSFORM_METHOD_NAME, TRANSFORM_SIGNATURE_DELEGATE, null,
				new String[] { EXCEPTION_CLASS });
		mv.visitCode();
		mv.visitInsn(ACONST_NULL);
		mv.visitInsn(ARETURN);
		mv.visitMaxs(1, 6);
		mv.visitEnd();
	}

	private void makePrimitiveClassDescriptor(final Class<?> type,
			final MethodVisitor mv) {

		if (type == boolean.class) {
			mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "TYPE",
					"Ljava/lang/Class;");

		} else if (type == byte.class) {
			mv.visitFieldInsn(GETSTATIC, "java/lang/Byte", "TYPE",
					"Ljava/lang/Class;");

		} else if (type == char.class) {
			mv.visitFieldInsn(GETSTATIC, "java/lang/Character", "TYPE",
					"Ljava/lang/Class;");

		} else if (type == short.class) {
			mv.visitFieldInsn(GETSTATIC, "java/lang/Short", "TYPE",
					"Ljava/lang/Class;");

		} else if (type == int.class) {
			mv.visitFieldInsn(GETSTATIC, "java/lang/Integer", "TYPE",
					"Ljava/lang/Class;");

		} else if (type == long.class) {
			mv.visitFieldInsn(GETSTATIC, "java/lang/Long", "TYPE",
					"Ljava/lang/Class;");

		} else if (type == float.class) {
			mv.visitFieldInsn(GETSTATIC, "java/lang/Float", "TYPE",
					"Ljava/lang/Class;");

		} else if (type == double.class) {
			mv.visitFieldInsn(GETSTATIC, "java/lang/Double", "TYPE",
					"Ljava/lang/Class;");
		}
	}

	private void makePrimitiveAutoboxing(final Class<?> type,
			final MethodVisitor mv) {

		if (type == boolean.class) {
			mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean");
			mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean",
					"booleanValue", "()Z");

		} else if (type == byte.class) {
			mv.visitTypeInsn(CHECKCAST, "java/lang/Byte");
			mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue",
					"()B");

		} else if (type == char.class) {
			mv.visitTypeInsn(CHECKCAST, "java/lang/Character");
			mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character",
					"charValue", "()C");

		} else if (type == short.class) {
			mv.visitTypeInsn(CHECKCAST, "java/lang/Short");
			mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue",
					"()S");

		} else if (type == int.class) {
			mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
			mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue",
					"()I");

		} else if (type == long.class) {
			mv.visitTypeInsn(CHECKCAST, "java/lang/Long");
			mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue",
					"()J");

		} else if (type == float.class) {
			mv.visitTypeInsn(CHECKCAST, "java/lang/Float");
			mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue",
					"()F");

		} else if (type == double.class) {
			mv.visitTypeInsn(CHECKCAST, "java/lang/Double");
			mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double",
					"doubleValue", "()D");
		}
	}
}
